<?php

/*  PEL: PHP Exif Library.  A library with support for reading and
 *  writing all Exif headers in JPEG and TIFF images using PHP.
 *
 *  Copyright (C) 2004, 2005, 2006  Martin Geisler.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program in the file COPYING; if not, write to the
 *  Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 *  Boston, MA 02110-1301 USA
 */

/* $Id: PelIfd.php 443 2006-09-17 18:32:04Z mgeisler $ */


/**
 * Classes for dealing with Exif IFDs.
 *
 * @author Martin Geisler <mgeisler@users.sourceforge.net>
 * @version $Revision: 443 $
 * @date $Date: 2006-09-17 20:32:04 +0200 (Sun, 17 Sep 2006) $
 * @license http://www.gnu.org/licenses/gpl.html GNU General Public
 * License (GPL)
 * @package PEL
 */

/**#@+ Required class definitions. */
require_once('PelEntryUndefined.php');
require_once('PelEntryRational.php');
require_once('PelDataWindow.php');
require_once('PelEntryAscii.php');
require_once('PelEntryShort.php');
require_once('PelEntryByte.php');
require_once('PelEntryLong.php');
require_once('PelException.php');
require_once('PelFormat.php');
require_once('PelEntry.php');
require_once('PelTag.php');
require_once('Pel.php');
/**#@-*/


/**
 * Exception indicating a general problem with the IFD.
 *
 * @author Martin Geisler <mgeisler@users.sourceforge.net>
 * @package PEL
 * @subpackage Exception
 */
class PelIfdException extends PelException {}

/**
 * Class representing an Image File Directory (IFD).
 *
 * {@link PelTiff TIFF data} is structured as a number of Image File
 * Directories, IFDs for short.  Each IFD contains a number of {@link
 * PelEntry entries}, some data and finally a link to the next IFD.
 *
 * @author Martin Geisler <mgeisler@users.sourceforge.net>
 * @package PEL
 */
class PelIfd implements IteratorAggregate, ArrayAccess {

  /**
   * Main image IFD.
   *
   * Pass this to the constructor when creating an IFD which will be
   * the IFD of the main image.
   */
  const IFD0 = 0;

  /**
   * Thumbnail image IFD.
   *
   * Pass this to the constructor when creating an IFD which will be
   * the IFD of the thumbnail image.
   */
  const IFD1 = 1;

  /**
   * Exif IFD.
   *
   * Pass this to the constructor when creating an IFD which will be
   * the Exif sub-IFD.
   */
  const EXIF = 2;

  /**
   * GPS IFD.
   *
   * Pass this to the constructor when creating an IFD which will be
   * the GPS sub-IFD.
   */
  const GPS  = 3;

  /**
   * Interoperability IFD.
   *
   * Pass this to the constructor when creating an IFD which will be
   * the interoperability sub-IFD.
   */
  const INTEROPERABILITY = 4;

  /**
   * The entries held by this directory.
   *
   * Each tag in the directory is represented by a {@link PelEntry}
   * object in this array.
   *
   * @var array
   */
  private $entries = array();

  /**
   * The type of this directory.
   *
   * Initialized in the constructor.  Must be one of {@link IFD0},
   * {@link IFD1}, {@link EXIF}, {@link GPS}, or {@link
   * INTEROPERABILITY}.
   *
   * @var int
   */
  private $type;

  /**
   * The next directory.
   *
   * This will be initialized in the constructor, or be left as null
   * if this is the last directory.
   *
   * @var PelIfd
   */
  private $next = null;

  /**
   * Sub-directories pointed to by this directory.
   *
   * This will be an array of ({@link PelTag}, {@link PelIfd}) pairs.
   *
   * @var array
   */
  private $sub = array();

  /**
   * The thumbnail data.
   *
   * This will be initialized in the constructor, or be left as null
   * if there are no thumbnail as part of this directory.
   *
   * @var PelDataWindow
   */
  private $thumb_data = null;
  // TODO: use this format to choose between the
  // JPEG_INTERCHANGE_FORMAT and STRIP_OFFSETS tags.
  // private $thumb_format;

  
  /**
   * Construct a new Image File Directory (IFD).
   *
   * The IFD will be empty, use the {@link addEntry()} method to add
   * an {@link PelEntry}.  Use the {@link setNext()} method to link
   * this IFD to another.
   *
   * @param int type the type of this IFD.  Must be one of {@link
   * IFD0}, {@link IFD1}, {@link EXIF}, {@link GPS}, or {@link
   * INTEROPERABILITY}.  An {@link PelIfdException} will be thrown
   * otherwise.
   */
  function __construct($type) {
    if ($type != PelIfd::IFD0 && $type != PelIfd::IFD1 &&
        $type != PelIfd::EXIF && $type != PelIfd::GPS &&
        $type != PelIfd::INTEROPERABILITY)
      throw new PelIfdException('Unknown IFD type: %d', $type);

    $this->type = $type;
  }


  /**
   * Load data into a Image File Directory (IFD).
   *
   * @param PelDataWindow the data window that will provide the data.
   *
   * @param int the offset within the window where the directory will
   * be found.
   */
  function load(PelDataWindow $d, $offset) {
    $thumb_offset = 0;
    $thumb_length = 0;

    Pel::debug('Constructing IFD at offset %d from %d bytes...',
               $offset, $d->getSize());

    /* Read the number of entries */
    $n = $d->getShort($offset);
    Pel::debug('Loading %d entries...', $n);
    
    $offset += 2;

    /* Check if we have enough data. */
    if ($offset + 12 * $n > $d->getSize()) {
      $n = floor(($offset - $d->getSize()) / 12);
      Pel::maybeThrow(new PelIfdException('Adjusted to: %d.', $n));
    }

    for ($i = 0; $i < $n; $i++) {
      // TODO: increment window start instead of using offsets.
      $tag = $d->getShort($offset + 12 * $i);
      Pel::debug('Loading entry with tag 0x%04X: %s (%d of %d)...',
                 $tag, PelTag::getName($this->type, $tag), $i + 1, $n);
      
      switch ($tag) {
      case PelTag::EXIF_IFD_POINTER:
      case PelTag::GPS_INFO_IFD_POINTER:
      case PelTag::INTEROPERABILITY_IFD_POINTER:
        $o = $d->getLong($offset + 12 * $i + 8);
        Pel::debug('Found sub IFD at offset %d', $o);

        /* Map tag to IFD type. */
        if ($tag == PelTag::EXIF_IFD_POINTER)
          $type = PelIfd::EXIF;
        elseif ($tag == PelTag::GPS_INFO_IFD_POINTER)
          $type = PelIfd::GPS;
        elseif ($tag == PelTag::INTEROPERABILITY_IFD_POINTER)
          $type = PelIfd::INTEROPERABILITY;

        $this->sub[$type] = new PelIfd($type);
        $this->sub[$type]->load($d, $o);
        break;
      case PelTag::JPEG_INTERCHANGE_FORMAT:
        $thumb_offset = $d->getLong($offset + 12 * $i + 8);
        $this->safeSetThumbnail($d, $thumb_offset, $thumb_length);
        break;
      case PelTag::JPEG_INTERCHANGE_FORMAT_LENGTH:
        $thumb_length = $d->getLong($offset + 12 * $i + 8);
        $this->safeSetThumbnail($d, $thumb_offset, $thumb_length);
        break;
      default:
        $format     = $d->getShort($offset + 12 * $i + 2);
        $components = $d->getLong($offset + 12 * $i + 4);
        
        /* The data size.  If bigger than 4 bytes, the actual data is
         * not in the entry but somewhere else, with the offset stored
         * in the entry.
         */
        $s = PelFormat::getSize($format) * $components;
        if ($s > 0) {    
          $doff = $offset + 12 * $i + 8;
          if ($s > 4)
            $doff = $d->getLong($doff);

          $data = $d->getClone($doff, $s);
        } else {
          $data = new PelDataWindow();
        }

        try {
            $entry = $this->newEntryFromData($tag, $format, $components, $data);

            if ($this->isValidTag($tag)) {
              $entry->setIfdType($this->type);
              $this->entries[$tag] = $entry;
            } else {
              Pel::maybeThrow(new PelInvalidDataException("IFD %s cannot hold\n%s",
                                                          $this->getName(),
                                                          $entry->__toString()));
            }
          } catch (PelException $e) {
            /* Throw the exception when running in strict mode, store
             * otherwise. */
            Pel::maybeThrow($e);
          }

        /* The format of the thumbnail is stored in this tag. */
//         TODO: handle TIFF thumbnail.
//         if ($tag == PelTag::COMPRESSION) {
//           $this->thumb_format = $data->getShort();
//         }
        break;
      }
    }

    /* Offset to next IFD */
    $o = $d->getLong($offset + 12 * $n);
    Pel::debug('Current offset is %d, link at %d points to %d.',
               $offset,  $offset + 12 * $n, $o);

    if ($o > 0) {
      /* Sanity check: we need 6 bytes  */
      if ($o > $d->getSize() - 6) {
        Pel::maybeThrow(new PelIfdException('Bogus offset to next IFD: ' .
                                            '%d > %d!',
                                            $o, $d->getSize() - 6));
      } else {
        if ($this->type == PelIfd::IFD1) // IFD1 shouldn't link further...
          Pel::maybeThrow(new PelIfdException('IFD1 links to another IFD!'));

        $this->next = new PelIfd(PelIfd::IFD1);
        $this->next->load($d, $o);
      }
    } else {
      Pel::debug('Last IFD.');
    }
  }


  /**
   * Make a new entry from a bunch of bytes.
   *
   * This method will create the proper subclass of {@link PelEntry}
   * corresponding to the {@link PelTag} and {@link PelFormat} given.
   * The entry will be initialized with the data given.
   *
   * Please note that the data you pass to this method should come
   * from an image, that is, it should be raw bytes.  If instead you
   * want to create an entry for holding, say, an short integer, then
   * create a {@link PelEntryShort} object directly and load the data
   * into it.
   *
   * A {@link PelUnexpectedFormatException} is thrown if a mismatch is
   * discovered between the tag and format, and likewise a {@link
   * PelWrongComponentCountException} is thrown if the number of
   * components does not match the requirements of the tag.  The
   * requirements for a given tag (if any) can be found in the
   * documentation for {@link PelTag}.
   *
   * @param PelTag the tag of the entry.
   *
   * @param PelFormat the format of the entry.
   *
   * @param int the components in the entry.
   *
   * @param PelDataWindow the data which will be used to construct the
   * entry.
   *
   * @return PelEntry a newly created entry, holding the data given.
   */
  function newEntryFromData($tag, $format, $components, PelDataWindow $data) {

    /* First handle tags for which we have a specific PelEntryXXX
     * class. */

    switch ($this->type) {

    case self::IFD0:
    case self::IFD1:
    case self::EXIF:
    case self::INTEROPERABILITY:

      switch ($tag) {
      case PelTag::DATE_TIME:
      case PelTag::DATE_TIME_ORIGINAL:
      case PelTag::DATE_TIME_DIGITIZED:
        if ($format != PelFormat::ASCII)
          throw new PelUnexpectedFormatException($this->type, $tag, $format,
                                               PelFormat::ASCII);

        if ($components != 20)
          throw new PelWrongComponentCountException($this->type, $tag, $components, 20);

        // TODO: handle timezones.
        return new PelEntryTime($tag, $data->getBytes(0, -1), PelEntryTime::EXIF_STRING);

      case PelTag::COPYRIGHT:
        if ($format != PelFormat::ASCII)
          throw new PelUnexpectedFormatException($this->type, $tag, $format,
                                                 PelFormat::ASCII);

        $v = explode("\0", trim($data->getBytes(), ' '));
        return new PelEntryCopyright($v[0], $v[1]);

      case PelTag::EXIF_VERSION:
      case PelTag::FLASH_PIX_VERSION:
      case PelTag::INTEROPERABILITY_VERSION:
        if ($format != PelFormat::UNDEFINED)
          throw new PelUnexpectedFormatException($this->type, $tag, $format,
                                               PelFormat::UNDEFINED);

        return new PelEntryVersion($tag, $data->getBytes() / 100);

      case PelTag::USER_COMMENT:
        if ($format != PelFormat::UNDEFINED)
          throw new PelUnexpectedFormatException($this->type, $tag, $format,
                                                 PelFormat::UNDEFINED);
        if ($data->getSize() < 8) {
          return new PelEntryUserComment();
        } else {
          return new PelEntryUserComment($data->getBytes(8),
                                       rtrim($data->getBytes(0, 8)));
        }

      case PelTag::XP_TITLE:
      case PelTag::XP_COMMENT:
      case PelTag::XP_AUTHOR:
      case PelTag::XP_KEYWORDS:
      case PelTag::XP_SUBJECT:
        if ($format != PelFormat::BYTE)
          throw new PelUnexpectedFormatException($this->type, $tag, $format,
                                               PelFormat::BYTE);

        $v = '';
        for ($i = 0; $i < $components; $i++) {
          $b = $data->getByte($i);
          /* Convert the byte to a character if it is non-null ---
           * information about the character encoding of these entries
           * would be very nice to have!  So far my tests have shown
           * that characters in the Latin-1 character set are stored in
           * a single byte followed by a NULL byte. */
          if ($b != 0)
            $v .= chr($b);
        }

        return new PelEntryWindowsString($tag, $v);
      }

    case self::GPS:
      
    default:
      /* Then handle the basic formats. */
      switch ($format) {
      case PelFormat::BYTE:
        $v =  new PelEntryByte($tag);
        for ($i = 0; $i < $components; $i++)
          $v->addNumber($data->getByte($i));
        return $v;

      case PelFormat::SBYTE:
        $v =  new PelEntrySByte($tag);
        for ($i = 0; $i < $components; $i++)
          $v->addNumber($data->getSByte($i));
        return $v;

      case PelFormat::ASCII:
        return new PelEntryAscii($tag, $data->getBytes(0, -1));

      case PelFormat::SHORT:
        $v =  new PelEntryShort($tag);
        for ($i = 0; $i < $components; $i++)
          $v->addNumber($data->getShort($i*2));
        return $v;

      case PelFormat::SSHORT:
        $v =  new PelEntrySShort($tag);
        for ($i = 0; $i < $components; $i++)
          $v->addNumber($data->getSShort($i*2));
        return $v;

      case PelFormat::LONG:
        $v =  new PelEntryLong($tag);
        for ($i = 0; $i < $components; $i++)
          $v->addNumber($data->getLong($i*4));
        return $v;

      case PelFormat::SLONG:
        $v =  new PelEntrySLong($tag);
        for ($i = 0; $i < $components; $i++)
          $v->addNumber($data->getSLong($i*4));
        return $v;

      case PelFormat::RATIONAL:
        $v =  new PelEntryRational($tag);
        for ($i = 0; $i < $components; $i++)
          $v->addNumber($data->getRational($i*8));
        return $v;

      case PelFormat::SRATIONAL:
        $v =  new PelEntrySRational($tag);
        for ($i = 0; $i < $components; $i++)
          $v->addNumber($data->getSRational($i*8));
        return $v;

      case PelFormat::UNDEFINED:
        return new PelEntryUndefined($tag, $data->getBytes());

      default:
        throw new PelException('Unsupported format: %s',
                               PelFormat::getName($format));
      }
    }
  }




  /**
   * Extract thumbnail data safely.
   *
   * It is safe to call this method repeatedly with either the offset
   * or the length set to zero, since it requires both of these
   * arguments to be positive before the thumbnail is extracted.
   *
   * When both parameters are set it will check the length against the
   * available data and adjust as necessary. Only then is the
   * thumbnail data loaded.
   *
   * @param PelDataWindow the data from which the thumbnail will be
   * extracted.
   *
   * @param int the offset into the data.
   *
   * @param int the length of the thumbnail.
   */
  private function safeSetThumbnail(PelDataWindow $d, $offset, $length) {
    /* Load the thumbnail if both the offset and the length is
     * available. */
    if ($offset > 0 && $length > 0) {
      /* Some images have a broken length, so we try to carefully
       * check the length before we store the thumbnail. */
      if ($offset + $length > $d->getSize()) {
        Pel::maybeThrow(new PelIfdException('Thumbnail length %d bytes ' .
                                            'adjusted to %d bytes.',
                                            $length,
                                            $d->getSize() - $offset));
        $length = $d->getSize() - $offset;
      }

      /* Now set the thumbnail normally. */
      $this->setThumbnail($d->getClone($offset, $length));
    }
  }

  
  /**
   * Set thumbnail data.
   *
   * Use this to embed an arbitrary JPEG image within this IFD. The
   * data will be checked to ensure that it has a proper {@link
   * PelJpegMarker::EOI} at the end.  If not, then the length is
   * adjusted until one if found.  An {@link PelIfdException} might be
   * thrown (depending on {@link Pel::$strict}) this case.
   *
   * @param PelDataWindow the thumbnail data.
   */
  function setThumbnail(PelDataWindow $d) {
    $size = $d->getSize();
    /* Now move backwards until we find the EOI JPEG marker. */
    while ($d->getByte($size - 2) != 0xFF ||
           $d->getByte($size - 1) != PelJpegMarker::EOI) {
      $size--;
    }

    if ($size != $d->getSize())
      Pel::maybeThrow(new PelIfdException('Decrementing thumbnail size ' .
                                          'to %d bytes', $size));
    
    $this->thumb_data = $d->getClone(0, $size);
  }


  /**
   * Get the type of this directory.
   *
   * @return int of {@link PelIfd::IFD0}, {@link PelIfd::IFD1}, {@link
   * PelIfd::EXIF}, {@link PelIfd::GPS}, or {@link
   * PelIfd::INTEROPERABILITY}.
   */
  function getType() {
    return $this->type;
  }


  /**
   * Is a given tag valid for this IFD?
   *
   * Different types of IFDs can contain different kinds of tags ---
   * the {@link IFD0} type, for example, cannot contain a {@link
   * PelTag::GPS_LONGITUDE} tag.
   *
   * A special exception is tags with values above 0xF000.  They are
   * treated as private tags and will be allowed everywhere (use this
   * for testing or for implementing your own types of tags).
   *
   * @param PelTag the tag.
   *
   * @return boolean true if the tag is considered valid in this IFD,
   * false otherwise.
   *
   * @see getValidTags()
   */
  function isValidTag($tag) {
    return $tag > 0xF000 || in_array($tag, $this->getValidTags());
  }


  /**
   * Returns a list of valid tags for this IFD.
   *
   * @return array an array of {@link PelTag}s which are valid for
   * this IFD.
   */
  function getValidTags() {
    switch ($this->type) {
    case PelIfd::IFD0:
    case PelIfd::IFD1:
      return array(PelTag::IMAGE_WIDTH,
                   PelTag::IMAGE_LENGTH,
                   PelTag::BITS_PER_SAMPLE,
                   PelTag::COMPRESSION,
                   PelTag::PHOTOMETRIC_INTERPRETATION,
                   PelTag::IMAGE_DESCRIPTION,
                   PelTag::MAKE,
                   PelTag::MODEL,
                   PelTag::STRIP_OFFSETS,
                   PelTag::ORIENTATION,
                   PelTag::SAMPLES_PER_PIXEL,
                   PelTag::ROWS_PER_STRIP,
                   PelTag::STRIP_BYTE_COUNTS,
                   PelTag::X_RESOLUTION,
                   PelTag::Y_RESOLUTION,
                   PelTag::PLANAR_CONFIGURATION,
                   PelTag::RESOLUTION_UNIT,
                   PelTag::TRANSFER_FUNCTION,
                   PelTag::SOFTWARE,
                   PelTag::DATE_TIME,
                   PelTag::ARTIST,
                   PelTag::WHITE_POINT,
                   PelTag::PRIMARY_CHROMATICITIES,
                   PelTag::JPEG_INTERCHANGE_FORMAT,
                   PelTag::JPEG_INTERCHANGE_FORMAT_LENGTH,
                   PelTag::YCBCR_COEFFICIENTS,
                   PelTag::YCBCR_SUB_SAMPLING,
                   PelTag::YCBCR_POSITIONING,
                   PelTag::REFERENCE_BLACK_WHITE,
                   PelTag::COPYRIGHT,
                   PelTag::EXIF_IFD_POINTER,
                   PelTag::GPS_INFO_IFD_POINTER,
                   PelTag::PRINT_IM);

    case PelIfd::EXIF:
      return array(PelTag::EXPOSURE_TIME,
                   PelTag::FNUMBER,
                   PelTag::EXPOSURE_PROGRAM,
                   PelTag::SPECTRAL_SENSITIVITY,
                   PelTag::ISO_SPEED_RATINGS,
                   PelTag::OECF,
                   PelTag::EXIF_VERSION,
                   PelTag::DATE_TIME_ORIGINAL,
                   PelTag::DATE_TIME_DIGITIZED,
                   PelTag::COMPONENTS_CONFIGURATION,
                   PelTag::COMPRESSED_BITS_PER_PIXEL,
                   PelTag::SHUTTER_SPEED_VALUE,
                   PelTag::APERTURE_VALUE,
                   PelTag::BRIGHTNESS_VALUE,
                   PelTag::EXPOSURE_BIAS_VALUE,
                   PelTag::MAX_APERTURE_VALUE,
                   PelTag::SUBJECT_DISTANCE,
                   PelTag::METERING_MODE,
                   PelTag::LIGHT_SOURCE,
                   PelTag::FLASH,
                   PelTag::FOCAL_LENGTH,
                   PelTag::MAKER_NOTE,
                   PelTag::USER_COMMENT,
                   PelTag::SUB_SEC_TIME,
                   PelTag::SUB_SEC_TIME_ORIGINAL,
                   PelTag::SUB_SEC_TIME_DIGITIZED,
                   PelTag::XP_TITLE,
                   PelTag::XP_COMMENT,
                   PelTag::XP_AUTHOR,
                   PelTag::XP_KEYWORDS,
                   PelTag::XP_SUBJECT,
                   PelTag::FLASH_PIX_VERSION,
                   PelTag::COLOR_SPACE,
                   PelTag::PIXEL_X_DIMENSION,
                   PelTag::PIXEL_Y_DIMENSION,
                   PelTag::RELATED_SOUND_FILE,
                   PelTag::FLASH_ENERGY,
                   PelTag::SPATIAL_FREQUENCY_RESPONSE,
                   PelTag::FOCAL_PLANE_X_RESOLUTION,
                   PelTag::FOCAL_PLANE_Y_RESOLUTION,
                   PelTag::FOCAL_PLANE_RESOLUTION_UNIT,
                   PelTag::SUBJECT_LOCATION,
                   PelTag::EXPOSURE_INDEX,
                   PelTag::SENSING_METHOD,
                   PelTag::FILE_SOURCE,
                   PelTag::SCENE_TYPE,
                   PelTag::CFA_PATTERN,
                   PelTag::CUSTOM_RENDERED,
                   PelTag::EXPOSURE_MODE,
                   PelTag::WHITE_BALANCE,
                   PelTag::DIGITAL_ZOOM_RATIO,
                   PelTag::FOCAL_LENGTH_IN_35MM_FILM,
                   PelTag::SCENE_CAPTURE_TYPE,
                   PelTag::GAIN_CONTROL,
                   PelTag::CONTRAST,
                   PelTag::SATURATION,
                   PelTag::SHARPNESS,
                   PelTag::DEVICE_SETTING_DESCRIPTION,
                   PelTag::SUBJECT_DISTANCE_RANGE,
                   PelTag::IMAGE_UNIQUE_ID,
                   PelTag::INTEROPERABILITY_IFD_POINTER,
                   PelTag::GAMMA);

    case PelIfd::GPS:
      return array(PelTag::GPS_VERSION_ID, 
                   PelTag::GPS_LATITUDE_REF, 
                   PelTag::GPS_LATITUDE, 
                   PelTag::GPS_LONGITUDE_REF, 
                   PelTag::GPS_LONGITUDE, 
                   PelTag::GPS_ALTITUDE_REF,
                   PelTag::GPS_ALTITUDE,
                   PelTag::GPS_TIME_STAMP,
                   PelTag::GPS_SATELLITES,
                   PelTag::GPS_STATUS,
                   PelTag::GPS_MEASURE_MODE,
                   PelTag::GPS_DOP,
                   PelTag::GPS_SPEED_REF,
                   PelTag::GPS_SPEED,
                   PelTag::GPS_TRACK_REF,
                   PelTag::GPS_TRACK,
                   PelTag::GPS_IMG_DIRECTION_REF,
                   PelTag::GPS_IMG_DIRECTION,
                   PelTag::GPS_MAP_DATUM,
                   PelTag::GPS_DEST_LATITUDE_REF,
                   PelTag::GPS_DEST_LATITUDE,
                   PelTag::GPS_DEST_LONGITUDE_REF,
                   PelTag::GPS_DEST_LONGITUDE,
                   PelTag::GPS_DEST_BEARING_REF,
                   PelTag::GPS_DEST_BEARING,
                   PelTag::GPS_DEST_DISTANCE_REF,
                   PelTag::GPS_DEST_DISTANCE,
                   PelTag::GPS_PROCESSING_METHOD,
                   PelTag::GPS_AREA_INFORMATION,
                   PelTag::GPS_DATE_STAMP,
                   PelTag::GPS_DIFFERENTIAL);

    case PelIfd::INTEROPERABILITY:
      return array(PelTag::INTEROPERABILITY_INDEX, 
                   PelTag::INTEROPERABILITY_VERSION,
                   PelTag::RELATED_IMAGE_FILE_FORMAT, 
                   PelTag::RELATED_IMAGE_WIDTH, 
                   PelTag::RELATED_IMAGE_LENGTH);

      /* TODO: Where do these tags belong?
PelTag::FILL_ORDER,
PelTag::DOCUMENT_NAME, 
PelTag::TRANSFER_RANGE, 
PelTag::JPEG_PROC, 
PelTag::BATTERY_LEVEL, 
PelTag::IPTC_NAA, 
PelTag::INTER_COLOR_PROFILE, 
PelTag::CFA_REPEAT_PATTERN_DIM, 
      */
    }
  }


  /**
   * Get the name of an IFD type.
   *
   * @param int one of {@link PelIfd::IFD0}, {@link PelIfd::IFD1},
   * {@link PelIfd::EXIF}, {@link PelIfd::GPS}, or {@link
   * PelIfd::INTEROPERABILITY}.
   *
   * @return string the name of type.
   */
  static function getTypeName($type) {
    switch ($type) {
    case self::IFD0:
      return '0';
    case self::IFD1:
      return '1';
    case self::EXIF:
      return 'Exif';
    case self::GPS:
      return 'GPS';
    case self::INTEROPERABILITY:
      return 'Interoperability';
    default:
      throw new PelIfdException('Unknown IFD type: %d', $type);
    }
  }


  /**
   * Get the name of this directory.
   *
   * @return string the name of this directory.
   */
  function getName() {
    return $this->getTypeName($this->type);
  }


  /**
   * Adds an entry to the directory.
   *
   * @param PelEntry the entry that will be added.
   *
   * @todo The entry will be identified with its tag, so each
   * directory can only contain one entry with each tag.  Is this a
   * bug?
   */
  function addEntry(PelEntry $e) {
    $this->entries[$e->getTag()] = $e;
  }


  /**
   * Does a given tag exist in this IFD?
   *
   * This methods is part of the ArrayAccess SPL interface for
   * overriding array access of objects, it allows you to check for
   * existance of an entry in the IFD:
   *
   * <code>
   * if (isset($ifd[PelTag::FNUMBER]))
   *   // ... do something with the F-number.
   * </code>
   *
   * @param PelTag the offset to check.
   *
   * @return boolean whether the tag exists.
   */
  function offsetExists($tag) {
    return isset($this->entries[$tag]);
  }


  /**
   * Retrieve a given tag from this IFD.
   *
   * This methods is part of the ArrayAccess SPL interface for
   * overriding array access of objects, it allows you to read entries
   * from the IFD the same was as for an array:
   *
   * <code>
   * $entry = $ifd[PelTag::FNUMBER];
   * </code>
   *
   * @param PelTag the tag to return.  It is an error to ask for a tag
   * which is not in the IFD, just like asking for a non-existant
   * array entry.
   *
   * @return PelEntry the entry.
   */
  function offsetGet($tag) {
    return $this->entries[$tag];
  }


  /**
   * Set or update a given tag in this IFD.
   *
   * This methods is part of the ArrayAccess SPL interface for
   * overriding array access of objects, it allows you to add new
   * entries or replace esisting entries by doing:
   *
   * <code>
   * $ifd[PelTag::EXPOSURE_BIAS_VALUE] = $entry;
   * </code>
   *
   * Note that the actual array index passed is ignored!  Instead the
   * {@link PelTag} from the entry is used.
   *
   * @param PelTag the offset to update.
   *
   * @param PelEntry the new value.
   */
  function offsetSet($tag, $e) {
    if ($e instanceof PelEntry) {
      $tag = $e->getTag();
      $this->entries[$tag] = $e;
    } else {
      throw new PelInvalidArgumentException('Argument "%s" must be a PelEntry.', $e);
    }
  }


  /**
   * Unset a given tag in this IFD.
   *
   * This methods is part of the ArrayAccess SPL interface for
   * overriding array access of objects, it allows you to delete
   * entries in the IFD by doing:
   *
   * <code>
   * unset($ifd[PelTag::EXPOSURE_BIAS_VALUE])
   * </code>
   *
   * @param PelTag the offset to delete.
   */
  function offsetUnset($tag) {
    unset($this->entries[$tag]);
  }


  /**
   * Retrieve an entry.
   *
   * @param PelTag the tag identifying the entry.
   *
   * @return PelEntry the entry associated with the tag, or null if no
   * such entry exists.
   */
  function getEntry($tag) {
    if (isset($this->entries[$tag]))
      return $this->entries[$tag];
    else
      return null;
  }


  /**
   * Returns all entries contained in this IFD.
   *
   * @return array an array of {@link PelEntry} objects, or rather
   * descendant classes.  The array has {@link PelTag}s as keys
   * and the entries as values.
   *
   * @see getEntry
   * @see getIterator
   */
  function getEntries() {
    return $this->entries;
  }

  
  /**
   * Return an iterator for all entries contained in this IFD.
   *
   * Used with foreach as in
   *
   * <code>
   * foreach ($ifd as $tag => $entry) {
   *   // $tag is now a PelTag and $entry is a PelEntry object.
   * }
   * </code>
   *
   * @return Iterator an iterator using the {@link PelTag tags} as
   * keys and the entries as values.
   */
  function getIterator() {
    return new ArrayIterator($this->entries);
  }
  

  /**
   * Returns available thumbnail data.
   *
   * @return string the bytes in the thumbnail, if any.  If the IFD
   * does not contain any thumbnail data, the empty string is
   * returned.
   *
   * @todo Throw an exception instead when no data is available?
   *
   * @todo Return the $this->thumb_data object instead of the bytes?
   */
  function getThumbnailData() {
    if ($this->thumb_data != null)
      return $this->thumb_data->getBytes();
    else
      return '';
  }
  

  /**
   * Make this directory point to a new directory.
   *
   * @param PelIfd the IFD that this directory will point to.
   */
  function setNextIfd(PelIfd $i) {
    $this->next = $i;
  }


  /**
   * Return the IFD pointed to by this directory.
   *
   * @return PelIfd the next IFD, following this IFD. If this is the
   * last IFD, null is returned.
   */
  function getNextIfd() {
    return $this->next;
  }


  /**
   * Check if this is the last IFD.
   *
   * @return boolean true if there are no following IFD, false
   * otherwise.
   */
  function isLastIfd() {
    return $this->next == null;
  }


  /**
   * Add a sub-IFD.
   *
   * Any previous sub-IFD of the same type will be overwritten.
   *
   * @param PelIfd the sub IFD.  The type of must be one of {@link
   * PelIfd::EXIF}, {@link PelIfd::GPS}, or {@link
   * PelIfd::INTEROPERABILITY}.
   */
  function addSubIfd(PelIfd $sub) {
    $this->sub[$sub->type] = $sub;
  }


  /**
   * Return a sub IFD.
   *
   * @param int the type of the sub IFD.  This must be one of {@link
   * PelIfd::EXIF}, {@link PelIfd::GPS}, or {@link
   * PelIfd::INTEROPERABILITY}.
   *
   * @return PelIfd the IFD associated with the type, or null if that
   * sub IFD does not exist.
   */
  function getSubIfd($type) {
    if (isset($this->sub[$type]))
      return $this->sub[$type];
    else
      return null;
  }


  /**
   * Get all sub IFDs.
   *
   * @return array an associative array with (IFD-type, {@link
   * PelIfd}) pairs.
   */
  function getSubIfds() {
    return $this->sub;
  }


  /**
   * Turn this directory into bytes.
   *
   * This directory will be turned into a byte string, with the
   * specified byte order.  The offsets will be calculated from the
   * offset given.
   *
   * @param int the offset of the first byte of this directory.
   *
   * @param PelByteOrder the byte order that should be used when
   * turning integers into bytes.  This should be one of {@link
   * PelConvert::LITTLE_ENDIAN} and {@link PelConvert::BIG_ENDIAN}.
   */
  function getBytes($offset, $order) {
    $bytes = '';
    $extra_bytes = '';

    Pel::debug('Bytes from IDF will start at offset %d within Exif data',
               $offset);
    
    $n = count($this->entries) + count($this->sub);
    if ($this->thumb_data != null) {
      /* We need two extra entries for the thumbnail offset and
       * length. */
      $n += 2;
    }

    $bytes .= PelConvert::shortToBytes($n, $order);

    /* Initialize offset of extra data.  This included the bytes
     * preceding this IFD, the bytes needed for the count of entries,
     * the entries themselves (and sub entries), the extra data in the
     * entries, and the IFD link.
     */
    $end = $offset + 2 + 12 * $n + 4;

    foreach ($this->entries as $tag => $entry) {
      /* Each entry is 12 bytes long. */
      $bytes .= PelConvert::shortToBytes($entry->getTag(), $order);
      $bytes .= PelConvert::shortToBytes($entry->getFormat(), $order);
      $bytes .= PelConvert::longToBytes($entry->getComponents(), $order);
      
      /*
       * Size? If bigger than 4 bytes, the actual data is not in
       * the entry but somewhere else.
       */
      $data = $entry->getBytes($order);
      $s = strlen($data);
      if ($s > 4) {
        Pel::debug('Data size %d too big, storing at offset %d instead.',
                   $s, $end);
        $bytes .= PelConvert::longToBytes($end, $order);
        $extra_bytes .= $data;
        $end += $s;
      } else {
        Pel::debug('Data size %d fits.', $s);
        /* Copy data directly, pad with NULL bytes as necessary to
         * fill out the four bytes available.*/
        $bytes .= $data . str_repeat(chr(0), 4 - $s);
      }
    }

    if ($this->thumb_data != null) {
      Pel::debug('Appending %d bytes of thumbnail data at %d',
                 $this->thumb_data->getSize(), $end);
      // TODO: make PelEntry a class that can be constructed with
      // arguments corresponding to the newt four lines.
      $bytes .= PelConvert::shortToBytes(PelTag::JPEG_INTERCHANGE_FORMAT_LENGTH,
                                         $order);
      $bytes .= PelConvert::shortToBytes(PelFormat::LONG, $order);
      $bytes .= PelConvert::longToBytes(1, $order);
      $bytes .= PelConvert::longToBytes($this->thumb_data->getSize(),
                                        $order);
      
      $bytes .= PelConvert::shortToBytes(PelTag::JPEG_INTERCHANGE_FORMAT,
                                         $order);
      $bytes .= PelConvert::shortToBytes(PelFormat::LONG, $order);
      $bytes .= PelConvert::longToBytes(1, $order);
      $bytes .= PelConvert::longToBytes($end, $order);
      
      $extra_bytes .= $this->thumb_data->getBytes();
      $end += $this->thumb_data->getSize();
    }

    
    /* Find bytes from sub IFDs. */
    $sub_bytes = '';
    foreach ($this->sub as $type => $sub) {
      if ($type == PelIfd::EXIF)
        $tag = PelTag::EXIF_IFD_POINTER;
      elseif ($type == PelIfd::GPS)
        $tag = PelTag::GPS_INFO_IFD_POINTER;
      elseif ($type == PelIfd::INTEROPERABILITY)
        $tag = PelTag::INTEROPERABILITY_IFD_POINTER;

      /* Make an aditional entry with the pointer. */
      $bytes .= PelConvert::shortToBytes($tag, $order);
      /* Next the format, which is always unsigned long. */
      $bytes .= PelConvert::shortToBytes(PelFormat::LONG, $order);
      /* There is only one component. */
      $bytes .= PelConvert::longToBytes(1, $order);

      $data = $sub->getBytes($end, $order);
      $s = strlen($data);
      $sub_bytes .= $data;

      $bytes .= PelConvert::longToBytes($end, $order);
      $end += $s;
    }

    /* Make link to next IFD, if any*/
    if ($this->isLastIFD()) {
      $link = 0;
    } else {
      $link = $end;
    }

    Pel::debug('Link to next IFD: %d', $link);
    
    $bytes .= PelConvert::longtoBytes($link, $order);

    $bytes .= $extra_bytes . $sub_bytes;

    if (!$this->isLastIfd())
      $bytes .= $this->next->getBytes($end, $order);

    return $bytes;
  }

  
  /**
   * Turn this directory into text.
   *
   * @return string information about the directory, mainly for
   * debugging.
   */
  function __toString() {
    $str = Pel::fmt("Dumping IFD %s with %d entries...\n",
                    $this->getName(), count($this->entries));
    
    foreach ($this->entries as $entry)
      $str .= $entry->__toString();

    $str .= Pel::fmt("Dumping %d sub IFDs...\n", count($this->sub));

    foreach ($this->sub as $type => $ifd)
      $str .= $ifd->__toString();

    if ($this->next != null)
      $str .= $this->next->__toString();

    return $str;
  }


}

?>